--- title: "05-Redis 底层原理与性能" created: 2026-08-31 tags: - 项目筑基 --- # Redis 底层原理与性能 > Redis 系统讲解第四篇:**为什么快**。书上看的原理篇读书笔记——SDS、渐进式 rehash、跳表、单线程模型、淘汰策略,最后落到"大 key / 热 key / 事务与 Lua"这些工程上真正会撞上的问题。 ## 为什么单线程还能这么快 ```text 纯内存操作(微秒级) + 单线程无锁无上下文切换 + IO 多路复用(epoll 同时监听万个连接) ``` 书里的经典比喻:多线程像多个窗口排队办业务(要协调),Redis 是一个超快的柜员 + 叫号系统(epoll)——**瓶颈从"调度"转移到"单个操作的耗时"**,这就是为什么大 key 是毒瘤:一个操作慢,所有请求排队。 Redis 6.0 的多线程**只用于网络读写和协议解析**,命令执行仍是单线程——所以"单线程模型"的原子性结论至今成立。 ## SDS:Redis 自己的字符串 C 字符串的三个痛点 → SDS 的三个设计: | C 字符串问题 | SDS 方案 | | --- | --- | | `strlen` 要 O(n) 扫到 \0 | 头部记录 len,取长度 O(1) | | 二进制不安全(\0 截断) | 按 len 读取,二进制安全(能存图片/序列化数据) | | 拼接必 realloc | 空间预分配 + 惰性释放,减少 realloc | ## dict 与渐进式 rehash Redis 的全局键空间就是一张 dict。扩容时不一次搬完(百万 key 搬一夜就卡死),而是**渐进式 rehash**:新旧两张表同时挂着,每次增删改查顺手搬一个桶,后台定时任务也帮着搬——期间读先查旧表再查新表,写只进新表。 > 💡 这是"把一次大卡顿摊薄成无数次小卡顿"的经典设计——和 Redis 一贯的"不能阻塞主线程"哲学一脉相承。 ## 跳表(skiplist):ZSet 的排序引擎 多层级链表:上层是下层的"快车道",查询从顶层往下贪心,期望 O(log n)。 书里对比过的"为什么不用平衡树/红黑树":跳表实现简单、范围查询天然友好(底层是有序链表,ZRANGE 顺着走就行)、增删只改前后指针不做旋转。Redis 作者的原话大意:**功能差不多时,选好写的那个**。 ZSet 实际是 **skiplist + dict 双结构**:dict 负责 member→score 的 O(1) 查分,skiplist 负责按 score 排序的范围查询——空间换时间。 ## listpack 与数据结构的"小数据紧凑化" 老版本用 ziplist(紧凑连续存储),缺陷是**连锁更新**(改一个元素可能引发后续所有节点的 prevlen 字段级联重写)。新版本逐步换成 **listpack**(每个元素只记自己的长度,天然无连锁更新)。配合 quicklist(List:双向链表把多个 listpack 串起来),构成"大数据用链式、小数据用紧凑"的通用思路。 ## 内存淘汰策略八选一 | 策略 | 范围 | 算法 | | --- | --- | --- | | noeviction(默认) | 不淘汰,内存满写报错 | — | | allkeys-lru / volatile-lru | 全部 / 仅带 TTL | 最近最少使用 | | allkeys-lfu / volatile-lfu | 同上 | 最不经常使用(4.0+,计数衰减) | | allkeys-random / volatile-random | 同上 | 随机 | | volatile-ttl | 仅带 TTL | 剩余寿命最短的先走 | LRU 是近似实现(采样 5 个 key 挑最久未用的,省内存);LFU 用 lru 字段的前 16 位存对数计数器 + 衰减。 ## 事务与 Lua:原子性但不回滚 `MULTI ... EXEC` 的两个反直觉点: - 入队报错(命令不存在)→ **整个事务不执行** - 执行时出错(类型错误,如对 List 执行 LPUSH 到 String)→ **其他命令照常执行,不回滚**——Redis 追求简单快速,不提供回滚 所以"逻辑性错误"防不住,**Lua 脚本才是真正的原子利器**:整段脚本单线程原子执行,还能做条件判断(锁校验、限流扣减都靠它)。脚本是原子的,但**不是隔离的"事务"**——脚本执行慢了照样阻塞所有人,Lua 里别写循环大逻辑。 ## 大 key 与热 key 治理 | 问题 | 危害 | 治理 | | --- | --- | --- | | 大 key | 读写阻塞、删除卡顿、迁移慢 | 拆分(Hash 分桶)、`UNLINK` 异步删、`--bigkeys` 排查 | | 热 key | 单 key 打满单线程 | 本地缓存兜一层、key 加后缀打散(读时随机挑一份)、读写分离 | > 💡 我的总记忆钩子:Redis 的性能哲学 = **永远别让主线程等**——所以异步删(UNLINK)、渐进式 rehash、bgsave 子进程、6.0 把网络 IO 挪到辅助线程,全是同一句话的不同实现。 --- ⬅️ [[04-Redis 持久化与高可用|04-Redis 持久化与高可用]] 🏠 [[00-数据库|00-数据库]] ➡️ [[02-mongoDB|02-mongoDB]]